/*
    Licensed to the Apache Software Foundation (ASF) under one
    or more contributor license agreements.  See the NOTICE file
    distributed with this work for additional information
    regarding copyright ownership.  The ASF licenses this file
    to you under the Apache License, Version 2.0 (the
    "License"); you may not use this file except in compliance
    with the License.  You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing,
    software distributed under the License is distributed on an
    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    KIND, either express or implied.  See the License for the
    specific language governing permissions and limitations
    under the License.
 */
package erwiki.part.rendering.internal.services;

import java.io.IOException;
import java.io.StringReader;
import java.lang.reflect.Constructor;
import java.util.Optional;

import org.apache.commons.lang3.time.StopWatch;
import org.apache.log4j.Logger;
import org.elwiki.IWikiConstants;
import org.elwiki_data.WikiPage;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ServiceScope;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventConstants;
import org.osgi.service.event.EventHandler;

import erwiki.LinkCollector;
import erwiki.api.WikiPartReference;
import erwiki.api.attachment.AttachmentManager;
import erwiki.api.component.WikiComponent;
import erwiki.api.core.Command;
import erwiki.api.core.Engine;
import erwiki.api.core.WikiContext;
import erwiki.api.event.PageEvent;
import erwiki.api.exceptions.FilterException;
import erwiki.api.exceptions.WikiException;
import erwiki.api.filters.FilterManager;
import erwiki.api.pages.PageManager;
import erwiki.api.parser.MarkupParser;
import erwiki.api.parser.WikiDocument;
import erwiki.api.render.RenderingManager;
import erwiki.api.render.WikiRenderer;
import erwiki.api.rwc.RWC;
import erwiki.api.ui.PageCommand;
import erwiki.api.variables.VariableManager;
import erwiki.configuration.IWikiConfiguration;
import erwiki.part.rendering.internal.parser.JSPWikiMarkupParser;
import erwiki.part.rendering.render.WysiwygEditingRenderer;
import erwiki.part.rendering.render.XHTMLRenderer;
import erwiki.util.ClassUtil;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;

/**
 * This class provides a facade towards the differing rendering routines. You should use the
 * routines in this manager instead of the ones in Engine, if you don't want the different side
 * effects to occur - such as WikiFilters.
 * <p>
 * This class also manages a rendering cache, i.e. documents are stored between calls. You may
 * control the cache by tweaking the ehcache.xml file.
 * <p>
 *
 * @since 2.4
 */
//@formatter:off
@Component(
	name = "erwiki.RenderingManager",
	service = { RenderingManager.class, WikiComponent.class, EventHandler.class },
	property = {
		EventConstants.EVENT_TOPIC + "=" + PageEvent.Topic.POST_SAVE_BEGIN,
	},
	scope = ServiceScope.SINGLETON)
//@formatter:on
public class DefaultRenderingManager implements RenderingManager, WikiComponent, EventHandler {

	private static final Logger log = Logger.getLogger(DefaultRenderingManager.class);

	/** The capacity of the caches, if you want something else, tweak ehcache.xml. */
	private static final int DEFAULT_CACHESIZE = 1_000;
	private static final String VERSION_DELIMITER = "::";

	/** The name of the default renderer. */
	private static final String DEFAULT_PARSER = JSPWikiMarkupParser.class.getName();
	/** The name of the default renderer. */
	private static final String DEFAULT_RENDERER = XHTMLRenderer.class.getName();
	/** The name of the default WYSIWYG renderer. */
	private static final String DEFAULT_WYSIWYG_RENDERER = WysiwygEditingRenderer.class.getName();

	private boolean m_useCache = true;
	private final CacheManager m_cacheManager = CacheManager.getInstance();
	private final int m_cacheExpiryPeriod = 24 * 60 * 60; // This can be relatively long

	/** Хранит WikiDocument, которые были кэшированы. */
	private Cache m_documentCache;

	private Constructor<?> m_rendererConstructor;
	private Constructor<?> m_rendererWysiwygConstructor;
	private String m_markupParserClass = DEFAULT_PARSER;

	// -- OSGi service handling ----------------------(start)--

	/** Stores configuration. */
	@Reference
	private IWikiConfiguration wikiConfiguration;

	@WikiPartReference
	private Engine m_engine;

	@WikiPartReference
	private AttachmentManager attachmentManager;

	@WikiPartReference
	private FilterManager filterManager;

	@WikiPartReference
	private PageManager pageManager;

	@WikiPartReference
	private VariableManager variableManager;

	/**
	 * {@inheritDoc}
	 *
	 * Checks for cache size settings, initializes the document cache. Looks for alternative
	 * WikiRenderers, initializes one, or the default XHTMLRenderer, for use.
	 */
	@Override
	public void initialize() throws WikiException {
		m_markupParserClass = wikiConfiguration.getStringProperty(PROP_PARSER, DEFAULT_PARSER);
		/*:FVK:
		if( !ClassUtil.assignable( m_markupParserClass, MarkupParser.class.getName() ) ) {
			log.warn( m_markupParserClass + " does not subclass " + MarkupParser.class.getName() + " reverting to default markup parser." );
			m_markupParserClass = DEFAULT_PARSER;
		}*/
		log.info("Using " + m_markupParserClass + " as markup parser.");

		m_useCache = false;// true; //:FVK: - removed option: TextUtil.getBooleanProperty(properties,
							// PageManager.PROP_USECACHE, true );
		if (m_useCache) {
			final String documentCacheName = m_engine.getApplicationName() + "." + DOCUMENTCACHE_NAME;
			if (m_cacheManager.cacheExists(documentCacheName)) {
				m_documentCache = m_cacheManager.getCache(documentCacheName);
			} else {
				log.info("cache with name " + documentCacheName
						+ " not found in ehcache.xml, creating it with defaults.");
				m_documentCache = new Cache(documentCacheName, DEFAULT_CACHESIZE, false, false, m_cacheExpiryPeriod,
						m_cacheExpiryPeriod);
				m_cacheManager.addCache(m_documentCache);
			}
		}

		final String renderImplName = wikiConfiguration.getStringProperty(PROP_RENDERER, DEFAULT_RENDERER);
		final String renderWysiwygImplName = wikiConfiguration.getStringProperty(PROP_WYSIWYG_RENDERER,
				DEFAULT_WYSIWYG_RENDERER);

		final Class<?>[] rendererParams = { WikiDocument.class };
		m_rendererConstructor = initRenderer(renderImplName, rendererParams);
		m_rendererWysiwygConstructor = initRenderer(renderWysiwygImplName, rendererParams);

		log.info("Rendering content with " + renderImplName + ".");
	}

	// -- OSGi service handling ------------------------(end)--

	private Constructor<?> initRenderer(final String renderImplName, final Class<?>[] rendererParams)
			throws WikiException {
		Constructor<?> c = null;
		try {
			final Class<?> clazz = Class.forName(renderImplName);
			c = clazz.getConstructor(rendererParams);
		} catch (final ClassNotFoundException e) {
			log.error("Unable to find WikiRenderer implementation " + renderImplName);
		} catch (final SecurityException e) {
			log.error("Unable to access the WikiRenderer(WikiDocument) constructor for " + renderImplName);
		} catch (final NoSuchMethodException e) {
			log.error("Unable to locate the WikiRenderer(WikiDocument) constructor for " + renderImplName);
		}
		if (c == null) {
			throw new WikiException("Failed to get WikiRenderer '" + renderImplName + "'.");
		}
		return c;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public MarkupParser getParser(final String pagedata) {
		// :FVK: WORKAROUND.
		if (1 == 1) {
			StringReader stringReader = new StringReader(pagedata);
			JSPWikiMarkupParser parser = new JSPWikiMarkupParser(stringReader);
			return parser;
		}

		try {
			return ClassUtil.getMappedObject(m_markupParserClass, new StringReader(pagedata));
		} catch (final ReflectiveOperationException | IllegalArgumentException e) {
			log.error("unable to get an instance of " + m_markupParserClass + " (" + e.getMessage()
					+ "), returning default markup parser.", e);
			return new JSPWikiMarkupParser(new StringReader(pagedata));
		} catch (Exception e) {
			System.out.println(e.getMessage());
		}
		return null; // :FVK: WORKAROUND.
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	// FIXME: The cache management policy is not very good: deleted/changed pages should be detected
	// better.
	public WikiDocument getRenderedDocument(final String pagedata) {
		String cacheKey = null;
		WikiPage realPage = RWC.INSTANCE.getRealPage();
		if (realPage != null) {
			cacheKey = String.join(VERSION_DELIMITER, realPage.getId(),
					// :FVK: context.getRealPage().getVersion(),
					RWC.INSTANCE.getVariable(WikiContext.VAR_EXECUTE_PLUGINS));
		}
		if (useCache()) {
			final Element element = m_documentCache.get(cacheKey);
			if (element != null) {
				final WikiDocument doc = (WikiDocument) element.getObjectValue();

				//
				// This check is needed in case the different filters have actually changed the page data.
				// FIXME: Figure out a faster method
				if (pagedata.equals(doc.getPageData())) {
					if (log.isDebugEnabled()) {
						log.debug("Using cached HTML for page " + cacheKey);
					}
					return doc;
				}
			} else if (log.isDebugEnabled()) {
				log.debug("Re-rendering and storing " + cacheKey);
			}
		}

		// Refresh the data content
		try {
			final MarkupParser parser = getParser(pagedata);
			final WikiDocument doc = parser.parse();
			doc.setPageData(pagedata);
			if (useCache()) {
				m_documentCache.put(new Element(cacheKey, doc));
			}
			return doc;
		} catch (final IOException ex) {
			log.error("Unable to parse", ex);
		}

		return null;
	}

	boolean useCache() {
		return m_useCache && WikiContext.PAGE_VIEW.equals(RWC.INSTANCE.getRequestContext());
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public String getHTML(final WikiDocument doc) throws IOException {
		Boolean wysiwygVariable = RWC.INSTANCE.getVariable(WikiContext.VAR_WYSIWYG_EDITOR_MODE);
		boolean wysiwygEditorMode = Optional.ofNullable(wysiwygVariable).orElse(false);

		WikiRenderer rend = (wysiwygEditorMode) ? getWysiwygRenderer(doc) : getRenderer(doc);

		return rend.getString();
	}

	/** {@inheritDoc} */
	@Override
	public String getHTML(final WikiPage page) {
		/*TODO::FVK: здесь где-то установлена в 'контекст' - требуемая версия содержимого страницы.
		  -- надо найти это место, а всё ли верно работает? */
		String pagedata = page.getContentText(RWC.INSTANCE.getPageVersion());

		for (String comment : page.getComments()) {
			// Add a line on top every each comment to separate it from each other.
			pagedata += "\n\n----\n\n" + comment;
		}

		return textToHTML(pagedata);
	}

	/**
	 * Возвращает преобразованный HTML-код конкретной версии содержимого страницы.</br>
	 * Версия должна быть положительным целым числом, в противном случае используется последняя версия.
	 *
	 * @param wikiPage страница для преобразования в HTML.
	 * @param version  Номер версии содержимого страницы, для извлечения текста.
	 * @return Текст страницы, отображаемый в формате HTML.
	 */
	@Override
	public String getHTML(final WikiPage wikiPage, int... version) {
		// :FVK: --было-- WikiContext context = Wiki.context().create( m_engine, wikiPage );
		// context.setRequestContext( WikiContext.PAGE_NONE );
		int requiredVersion = (version.length == 0 || version[0] == IWikiConstants.LATEST_VERSION)
				? wikiPage.getLastVersion()
				: version[0];
		RWC.INSTANCE.setPageVersion(requiredVersion);

		Command saved = RWC.INSTANCE.getCommand();
		try {
			RWC.INSTANCE.setCommand(PageCommand.NONE);
			return getHTML(wikiPage);
		} finally {
			RWC.INSTANCE.setCommand(saved);
		}
	}

	/** {@inheritDoc} */
	@Override
	public String textToHTML(String pagedata) {
		String result = "";

		String valueRunFilters = this.variableManager.getValue(VariableManager.VAR_RUNFILTERS, "true");
		boolean runFilters = "true".equals(valueRunFilters);

		StopWatch sw = new StopWatch();
		sw.start();
		try {
			if (runFilters) {
				pagedata = this.filterManager.doPreTranslateFiltering(pagedata);
			}

			result = getHTML(pagedata);

			if (runFilters) {
				result = this.filterManager.doPostTranslateFiltering(result);
			}
		} catch (FilterException e) {
			log.error("page filter threw exception: ", e);
			// FIXME: Don't yet know what to do
		} finally {
			sw.stop();
		}
		log.debug("Page " + RWC.INSTANCE.getRealPage().getName() + " rendered, took " + sw);

		return result;
	}

	/**
	 * Convenience method for rendering, using the default parser and renderer. Note that you can't use
	 * this method to do any arbitrary rendering, as the pagedata MUST be the data from the that the
	 * WikiContext refers to - this method caches the HTML internally, and will return the cached
	 * version. If the pagedata is different from what was cached, will re-render and store the pagedata
	 * into the internal cache.
	 *
	 * @param pagedata the page data
	 *
	 * @return XHTML data.
	 */
	private String getHTML(final String pagedata) {
		try {
			final WikiDocument doc = getRenderedDocument(pagedata);
			return getHTML(doc);
		} catch (final IOException e) {
			Logger.getLogger(RenderingManager.class).error("Unable to parse", e);
		}

		return null;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public String textToHTML(String pagedata, LinkCollector localLinkCollector, LinkCollector extLinkCollector,
			LinkCollector attLinkCollector, LinkCollector unknownPagesLinkCollector, boolean parseAccessRules,
			boolean justParse) {
		String result = "";

		if (pagedata == null) {
			log.error("NULL pagedata to textToHTML()");
			return null;
		}

		final boolean runFilters = "true".equals(this.variableManager.getValue(VariableManager.VAR_RUNFILTERS, "true"));

		try {
			final StopWatch sw = new StopWatch();
			sw.start();

			if (runFilters && this.filterManager != null) {
				pagedata = this.filterManager.doPreTranslateFiltering(pagedata);
			}

			final MarkupParser mp = getParser(pagedata);
			mp.addLocalLinkCollector(localLinkCollector);
			mp.addExternalLinkCollector(extLinkCollector);
			mp.addAttachmentLinkCollector(attLinkCollector);
			mp.addUnknownPagesLinkCollector(unknownPagesLinkCollector);

			if (!parseAccessRules) {
				mp.disableAccessRules();
			}

			final WikiDocument doc = mp.parse();

			// In some cases it's better just to parse, not to render
			if (!justParse) {
				result = getHTML(doc);

				if (runFilters && this.filterManager != null) {
					result = this.filterManager.doPostTranslateFiltering(result);
				}
			}

			sw.stop();

			if (log.isDebugEnabled()) {
				log.debug("Page " + RWC.INSTANCE.getRealPage().getName() + " rendered, took " + sw);
			}
		} catch (final IOException e) {
			log.error("Failed to scan page data: ", e);
		} catch (final FilterException e) {
			log.error("page filter threw exception: ", e);
			// FIXME: Don't yet know what to do
		}

		return result;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public WikiRenderer getRenderer(final WikiDocument doc) {
		final Object[] params = { doc };
		return getRenderer(params, m_rendererConstructor);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public WikiRenderer getWysiwygRenderer(final WikiDocument doc) {
		final Object[] params = { doc };
		return getRenderer(params, m_rendererWysiwygConstructor);
	}

	@SuppressWarnings("unchecked")
	private <T extends WikiRenderer> T getRenderer(final Object[] params, final Constructor<?> rendererConstructor) {
		try {
			return (T) rendererConstructor.newInstance(params);
		} catch (final Exception e) {
			log.error("Unable to create WikiRenderer", e);
		}
		return null;
	}

	@Override
	public void handleEvent(Event event) {
		String topic = event.getTopic();
		switch (topic) {
		case PageEvent.Topic.POST_SAVE_BEGIN:
			if (m_useCache) {
				/* Flushes the document cache in response to a POST_SAVE_BEGIN event. */
				WikiContext context = (WikiContext) event.getProperty(PageEvent.PROPERTY_WIKI_CONTEXT);
				WikiPage wikiPage = context.getPage();
				if (m_documentCache != null) {
					String cacheKey = wikiPage.getId();
					// :FVK: + VERSION_DELIMITER + context.getRealPage().getVersion()
					context.getVariable(WikiContext.VAR_EXECUTE_PLUGINS);
					m_documentCache.remove(cacheKey + VERSION_DELIMITER + Boolean.FALSE);
					m_documentCache.remove(cacheKey + VERSION_DELIMITER + Boolean.TRUE);
				}
			}
			break;
		}
	}

}
